Skip to content

Fix: enforce facility authorization in update_identifier endpoint#3560

Open
RCcoders wants to merge 3 commits intoohcnetwork:developfrom
RCcoders:fix-patient-identifier-authz
Open

Fix: enforce facility authorization in update_identifier endpoint#3560
RCcoders wants to merge 3 commits intoohcnetwork:developfrom
RCcoders:fix-patient-identifier-authz

Conversation

@RCcoders
Copy link

@RCcoders RCcoders commented Mar 5, 2026

Summary

The update_identifier endpoint retrieved PatientIdentifierConfig
using only the external_id without validating the facility relationship.

This could allow cross-facility identifier updates if a valid UUID
was known.

Fix

Added facility ownership validation before updating identifiers:

  • Verify identifier configuration belongs to the same facility as the patient
  • Enforce authorization using AuthorizationController

Testing

  • Ran Ruff lint checks
  • Executed full test suite (1412 tests) – all passed
  • Verified endpoint behaviour locally

This resolves the TODO comment in the original code:

# TODO: Check Facility Authz

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened authorization for patient identifier updates to enforce facility-scoped checks; cross-facility update attempts without facility write permission are now rejected with Forbidden and blocked from creating identifiers.
  • Tests

    • Added API tests that validate unauthorized identifier update attempts are denied and confirm no identifiers are created as a result.

@RCcoders RCcoders requested a review from a team as a code owner March 5, 2026 19:19
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

📝 Walkthrough

Walkthrough

Adds facility-scoped authorization checks to patient.identifier updates (enforcing facility ownership and write permission) and introduces a security test that verifies cross‑facility identifier update attempts are rejected with 403 and do not create identifiers.

Changes

Cohort / File(s) Summary
Authorization enforcement
care/emr/api/viewsets/patient.py
Added facility-context validation in update_identifier: ensures the requested PatientIdentifierConfig belongs to the patient's facility and calls AuthorizationController.can_write_facility_patient_identifier_config; raises PermissionDenied when mismatched or unauthorized.
Security test suite
care/emr/tests/test_patient_security_api.py
Added TestPatientSecurityAPI with setUp and test_update_identifier_bola_vulnerability that posts an update using a config from another facility, asserts a 403 response, and verifies no PatientIdentifier was created.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the issue, fix, and testing performed but lacks the structured sections defined in the template (Proposed Changes, Associated Issue, Merge Checklist). Consider restructuring the description to match the repository template with sections for Proposed Changes, Associated Issue, and a completed Merge Checklist for future PRs.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the primary change: enforcing facility authorization in the update_identifier endpoint.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
care/emr/api/viewsets/patient.py (2)

403-420: Authorization checks look appropriate, but there's a subtle edge case to consider.

The facility-context check on line 407 only triggers when both request_config.facility and facility are present. If the request omits a facility parameter, facility will be None, and this check silently passes.

Fortunately, the second check (lines 413-420) validates user permission against request_config.facility regardless, so the authorization is still enforced. Just worth noting that the first check is more of a context-consistency validation than a security gate.

One thought: should the endpoint require a facility context when the config has a facility? Currently, if request_config.facility is set but no facility context is provided, the request proceeds to the permission check (which will likely fail anyway). It might be cleaner to be explicit:

💡 Optional: Make facility context required when config has facility
         # Ensure identifier config belongs to the patient's facility context
-        if request_config.facility and facility and request_config.facility.id != facility.id:
+        if request_config.facility:
+            if not facility:
+                raise PermissionDenied(
+                    "Facility context is required for facility-scoped identifiers"
+                )
+            if request_config.facility.id != facility.id:
+                raise PermissionDenied(
+                    "Identifier configuration does not belong to the patient's facility"
+                )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/api/viewsets/patient.py` around lines 403 - 420, The current logic
allows requests where request_config.facility is set but the incoming facility
context (variable facility from get_serializer_list_context()) is missing; add
an explicit guard so when request_config.facility is present and facility is
None you raise a clear error (e.g., ValidationError or PermissionDenied) stating
that a facility context is required for facility-scoped identifier configs; keep
the existing
AuthorizationController.call("can_write_facility_patient_identifier_config",
self.request.user, request_config.facility) check intact so permission
enforcement still runs afterward.

407-420: Consider extracting exception messages to constants (per static analysis hint).

The Ruff TRY003 warnings suggest avoiding long inline messages. While not critical, extracting these to constants would be marginally more maintainable—if you ever felt so inclined.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/api/viewsets/patient.py` around lines 407 - 420, Extract the inline
PermissionDenied messages into module-level constants and use those constants in
the raises: create descriptive constants like IDENTIFIER_CONFIGURATION_MISMATCH
= "Identifier configuration does not belong to the patient's facility" and
CANNOT_UPDATE_IDENTIFIER_FOR_FACILITY = "Cannot update identifier for this
facility" (or similar) at the top of the patient.py module, then replace the
string literals inside the PermissionDenied raises in the block around
request_config/facility checks and the AuthorizationController.call check to
reference those constants instead.
care/emr/tests/test_patient_security_api.py (2)

20-25: Permissions granted don't include the one being checked.

The test grants can_write_patient and can_list_patients, but the authorization in patient.py checks for can_write_patient_identifier_config (via can_write_facility_patient_identifier_config).

The test still validates the 403 behavior correctly (since the user lacks the required permission entirely), but a more precise test would grant the correct permission for Facility A and verify it's denied for Facility B. Just something to consider for test clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/tests/test_patient_security_api.py` around lines 20 - 25, The test
currently grants PatientPermissions.can_write_patient and can_list_patients but
the authorization in patient.py checks for the facility-scoped permission
(can_write_facility_patient_identifier_config); update the role creation in
create_role_with_permissions to grant
PatientPermissions.can_write_facility_patient_identifier_config.name scoped to
Facility A (so the user has the correct permission for Facility A) and then
explicitly call the API for Facility B to assert a 403, ensuring the test
verifies allowed-for-A but denied-for-B behavior; reference
create_role_with_permissions, PatientPermissions and the facility check in
patient.py to locate the code to change.

16-17: org_b is created but never used.

Perhaps it was intended for a future test case, or it slipped through during implementation. Either way, it's just sitting there, not doing much.

🧹 Proposed cleanup
         self.org_a = self.create_organization(org_type="govt")
-        self.org_b = self.create_organization(org_type="govt")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/tests/test_patient_security_api.py` around lines 16 - 17, The test
creates self.org_b via create_organization but never uses it; remove the unused
setup to clean the test by deleting the self.org_b =
self.create_organization(org_type="govt") line (and any references to self.org_b
in setUp/teardown) in test_patient_security_api.py, or if it was intended to be
used, replace its removal by actually using self.org_b in the relevant
assertions — locate the creation in the test setup where create_organization is
called to make the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@care/emr/tests/test_patient_security_api.py`:
- Around line 31-34: The test named test_update_identifier_bola_vulnerability
has a docstring that incorrectly states the user "can" update an identifier from
another facility while the assertion expects a 403; update the docstring to
state that the user "cannot" update (or is forbidden from updating) an
identifier using a config from another facility so the description matches the
expected behavior in test_update_identifier_bola_vulnerability.

---

Nitpick comments:
In `@care/emr/api/viewsets/patient.py`:
- Around line 403-420: The current logic allows requests where
request_config.facility is set but the incoming facility context (variable
facility from get_serializer_list_context()) is missing; add an explicit guard
so when request_config.facility is present and facility is None you raise a
clear error (e.g., ValidationError or PermissionDenied) stating that a facility
context is required for facility-scoped identifier configs; keep the existing
AuthorizationController.call("can_write_facility_patient_identifier_config",
self.request.user, request_config.facility) check intact so permission
enforcement still runs afterward.
- Around line 407-420: Extract the inline PermissionDenied messages into
module-level constants and use those constants in the raises: create descriptive
constants like IDENTIFIER_CONFIGURATION_MISMATCH = "Identifier configuration
does not belong to the patient's facility" and
CANNOT_UPDATE_IDENTIFIER_FOR_FACILITY = "Cannot update identifier for this
facility" (or similar) at the top of the patient.py module, then replace the
string literals inside the PermissionDenied raises in the block around
request_config/facility checks and the AuthorizationController.call check to
reference those constants instead.

In `@care/emr/tests/test_patient_security_api.py`:
- Around line 20-25: The test currently grants
PatientPermissions.can_write_patient and can_list_patients but the authorization
in patient.py checks for the facility-scoped permission
(can_write_facility_patient_identifier_config); update the role creation in
create_role_with_permissions to grant
PatientPermissions.can_write_facility_patient_identifier_config.name scoped to
Facility A (so the user has the correct permission for Facility A) and then
explicitly call the API for Facility B to assert a 403, ensuring the test
verifies allowed-for-A but denied-for-B behavior; reference
create_role_with_permissions, PatientPermissions and the facility check in
patient.py to locate the code to change.
- Around line 16-17: The test creates self.org_b via create_organization but
never uses it; remove the unused setup to clean the test by deleting the
self.org_b = self.create_organization(org_type="govt") line (and any references
to self.org_b in setUp/teardown) in test_patient_security_api.py, or if it was
intended to be used, replace its removal by actually using self.org_b in the
relevant assertions — locate the creation in the test setup where
create_organization is called to make the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b40b982b-1aeb-42c2-9324-22dac3e9b0cc

📥 Commits

Reviewing files that changed from the base of the PR and between 82591d7 and af18e95.

📒 Files selected for processing (2)
  • care/emr/api/viewsets/patient.py
  • care/emr/tests/test_patient_security_api.py

@RCcoders
Copy link
Author

RCcoders commented Mar 6, 2026

Thanks for the review!

I've addressed the CodeRabbit feedback:

  • Updated the test docstring
  • Removed the unused org_b setup

All tests pass locally.
Please let me know if any further changes are required.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
care/emr/tests/test_patient_security_api.py (2)

19-25: Indentation of the permissions list is inconsistent.

The list items appear to have drifted leftward, which doesn't quite align with PEP 8 conventions. I'm sure it was just a minor slip.

🧹 Suggested formatting fix
         self.role = self.create_role_with_permissions(
             permissions=[
-    PatientPermissions.can_write_facility_patient_identifier_config.name,
-    PatientPermissions.can_write_patient.name,
-    PatientPermissions.can_list_patients.name,
-    ]
+                PatientPermissions.can_write_facility_patient_identifier_config.name,
+                PatientPermissions.can_write_patient.name,
+                PatientPermissions.can_list_patients.name,
+            ]
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/tests/test_patient_security_api.py` around lines 19 - 25, The
permissions list passed to create_role_with_permissions is mis-indented; adjust
the bracketed list so each item
(PatientPermissions.can_write_facility_patient_identifier_config.name,
PatientPermissions.can_write_patient.name,
PatientPermissions.can_list_patients.name) is indented consistently and aligned
under the opening bracket per PEP 8 (same indentation level for each element)
and ensure the closing bracket lines up with the start of the argument list in
the create_role_with_permissions call.

13-13: Remove the unused facility_a variable for clarity.

The variable is created but never referenced in the test. Only facility_b is actually used in test_update_identifier_bola_vulnerability. The setup comment "User has access to Facility A" appears to be vestigial—the user's access is controlled through self.org_a and the permission system, not facility_a. Removing it would eliminate the confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/tests/test_patient_security_api.py` at line 13, The test setup
creates an unused facility variable self.facility_a via create_facility which is
never referenced—remove the unused assignment to self.facility_a from the setup
so only the needed facility_b remains; update any setup comments (e.g., "User
has access to Facility A") to reflect that user access is controlled via
self.org_a and the permission system, and ensure
test_update_identifier_bola_vulnerability still uses self.facility_b and
self.org_a as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@care/emr/tests/test_patient_security_api.py`:
- Around line 19-25: The permissions list passed to create_role_with_permissions
is mis-indented; adjust the bracketed list so each item
(PatientPermissions.can_write_facility_patient_identifier_config.name,
PatientPermissions.can_write_patient.name,
PatientPermissions.can_list_patients.name) is indented consistently and aligned
under the opening bracket per PEP 8 (same indentation level for each element)
and ensure the closing bracket lines up with the start of the argument list in
the create_role_with_permissions call.
- Line 13: The test setup creates an unused facility variable self.facility_a
via create_facility which is never referenced—remove the unused assignment to
self.facility_a from the setup so only the needed facility_b remains; update any
setup comments (e.g., "User has access to Facility A") to reflect that user
access is controlled via self.org_a and the permission system, and ensure
test_update_identifier_bola_vulnerability still uses self.facility_b and
self.org_a as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bbd418dd-1a31-40cc-ac3c-36c213a660cb

📥 Commits

Reviewing files that changed from the base of the PR and between 84cf7d1 and 544a81e.

📒 Files selected for processing (1)
  • care/emr/tests/test_patient_security_api.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants